﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Upreader.Application.Model;
using System.Threading.Tasks;
using System.IO;
using System.Diagnostics;

namespace Upreader.Engine
{
    struct CachedSegmentDataStoreEntry
    {
        public Segment Segment { get; set; }

        public Stream CachedStream { get; set; }
    }

    public class SegmentDataStore
    {
        #region Fields

        private readonly object _syncRoot = new object();
        private readonly List<CachedSegmentDataStoreEntry> _cachedEntries;
        private readonly int _maxCachedEntries;
        
        private bool _closed;

        #endregion // Fields

        #region Constructor

        public SegmentDataStore(int cacheSize)
        {
            if (cacheSize < 0)
                throw new ArgumentOutOfRangeException("CacheSize can't be smaller than 1");

            _cachedEntries = new List<CachedSegmentDataStoreEntry>(cacheSize);
            _maxCachedEntries = cacheSize;
        }

        #endregion // Constructor

        #region Properties

        public bool Closed { get { return _closed; } }

        #endregion // Properties

        #region Methods

        public void StoreSegment(Segment segment, Stream segmentStream)
        {
            if (segment == null)
                throw new ArgumentNullException("segment");
            if (segmentStream == null)
                throw new ArgumentNullException("stream");

            if (Closed)
                throw new InvalidOperationException("The segmentDataStore is already closed");

            // only when caching is enabled
            if (_maxCachedEntries > 0)
            {
                lock (_syncRoot)
                {
                    if (_cachedEntries.Count > _maxCachedEntries)
                    {
                        // there is no space in the cache, persist the oldest entry to disk
                        var oldestEntry = _cachedEntries.First();
                        _cachedEntries.RemoveAt(0);

                        string segmentFilePath = CreateSegmentFilePath(oldestEntry.Segment);

                        Trace.WriteLine(string.Format("Persisting segment {0} for file {1} to disk since it's not yet required", segment.Index, segment.FileId));
                        PersistToDisk(segmentFilePath, oldestEntry.Segment, oldestEntry.CachedStream);
                    }

                    // add the cached entry to the queue
                    _cachedEntries.Add(new CachedSegmentDataStoreEntry()
                    {
                        Segment = segment,
                        CachedStream = segmentStream
                    });
                }
            }
            else
            {
                string segmentFilePath = CreateSegmentFilePath(segment);

                // no caching so directly persist to disk
                PersistToDisk(segmentFilePath, segment, segmentStream);
            }
        }

        /// <summary>
        /// Returns the segment dataStream. 
        /// </summary>
        public Stream LeaseSegment(Segment segment)
        {
            if (segment == null)
                throw new ArgumentNullException("segment");

            Stream resultStream = null;

               // only when caching is enabled
            if (_maxCachedEntries > 0)
            {
                lock (_syncRoot)
                {
                    // this should yield only 1 entry. 
                    resultStream = _cachedEntries.Where(x => x.Segment.Id == segment.Id)
                                                 .Select(x => x.CachedStream)
                                                 .FirstOrDefault();
                }
            }
            if (resultStream == null)
            {
                // try and locate on the disk
                string segmentFilePath = CreateSegmentFilePath(segment);
                // no caching so directly persist to disk
                resultStream = RestoreFromDisk(segmentFilePath);
            }

            if (resultStream == null)
            {
                throw new InvalidOperationException("No cached segment found with the given segmentId");
            }
            
            return resultStream;
        }

        /// <summary>
        /// Cleans up/forgets about a previously stored segment
        /// </summary>
        public void CleanSegment(Segment segment)
        {
            if (segment == null)
                throw new ArgumentNullException("segment");

            bool found = false;

            // only when caching is enabled
            if (_maxCachedEntries > 0)
            {
                lock (_syncRoot)
                {
                    // this should yield only 1 entry. 
                    IEnumerable<CachedSegmentDataStoreEntry> entries = _cachedEntries.Where(x => x.Segment.Id == segment.Id);
                    // copy to memory since we are about the modify the original collection
                    entries = entries.ToArray();
                    foreach (CachedSegmentDataStoreEntry entry in entries)
                    {
                        _cachedEntries.Remove(entry);
                    }
                }
            }
            if (!found)
            {
                // try and locate on the disk
                string segmentFilePath = CreateSegmentFilePath(segment);
                // no caching so directly persist from disk
                System.IO.File.Delete(segmentFilePath);

                found = true;
            }

            if (!found)
            {
                throw new InvalidOperationException("No cached segment found with the given segmentId");
            }
        }

        /// <summary>
        /// Closes the segmentDataStore. Since gracefully, each cached entry will be persisted to the filesystem prior to closing
        /// </summary>
        public void Close()
        {
            if (Closed)
                throw new InvalidOperationException("The segmentDataStore is already closed");

            _closed = true;

            lock (_syncRoot)
            {
                foreach (var cachedEntry in _cachedEntries)
                {
                    string segmentFilePath = CreateSegmentFilePath(cachedEntry.Segment);

                    // write it to disk
                    PersistToDisk(segmentFilePath, cachedEntry.Segment, cachedEntry.CachedStream);

                    // and close the cached stream to clean up resources
                    cachedEntry.CachedStream.Dispose();
                }
            }
        }

        #endregion // Methods

        #region Helpers

        private void PersistToDisk(string segmentFilePath, Segment segment, Stream stream)
        {
            EnsureDirectory(segmentFilePath);

            using (FileStream fileStream = System.IO.File.Open(segmentFilePath, FileMode.Create, FileAccess.Write))
            {
                stream.CopyTo(fileStream);
            }

            // clean up the original stream
            stream.Close();
        }

        private void EnsureDirectory(string segmentFilePath)
        {
            string directoryPath = Path.GetDirectoryName(segmentFilePath);

            if (!Directory.Exists(directoryPath))
                Directory.CreateDirectory(directoryPath);
        }

        /// <summary>
        /// Reads the segment from the disk (if found) and removes the original file
        /// </summary>
        private Stream RestoreFromDisk(string segmentFilePath)
        {
            try
            {
                FileStream fileStream = System.IO.File.Open(segmentFilePath, FileMode.Open, FileAccess.Read);

                return fileStream;
            }
            catch (FileNotFoundException)
            {
                // swallow filenotfound exceptions and simply return null
                return null;
            }
        }

        private string CreateSegmentFilePath(Segment segment)
        {
            if (segment.CachedFileName == null)
                segment.CachedFileName = Path.GetTempFileName();
            
            // todo: is it safe to use the id of the segment?
            return segment.CachedFileName;
        }

        #endregion // Helpers
    }
}
